package XMFramework.Core
{
	import Interfaces.MIAppStarter;
	
	import XMFramework.Interface.INetwork;
	
	import flash.events.AsyncErrorEvent;
	import flash.events.IOErrorEvent;
	import flash.events.NetStatusEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.NetConnection;
	import flash.net.ObjectEncoding;
	import flash.net.Responder;
	import flash.system.Security;
	import flash.system.System;
	import flash.utils.clearTimeout;
	import flash.utils.setTimeout;

	internal class SNetwork implements INetwork
	{
		// Communicate to server by AMF protocal (AMFPHP) 
		private var service : NetConnection		=	null;
		
		// Store server service info 
		private var serverInfo : Object;
		
		private var hostObject : MIAppStarter;
		
		private var globalResponseListener : Function	=	null;
		
		private var netStatusCallbackFunc : Function	=	null;	
		
		// Time left message is sent
		private var requestTimeDestination : Number	=	ShareConstant.NET_PRIORITY_MEDIUM;
		
		// Mark time elapse to ensure message is sent 
		private var requestTimeMark : Number	=	0.0;
		
		// Store request message data
		private var reqBucketData : Array		=	new Array();
		
		// Use for error handling. Try to request again with last params data
		private var lastSentParamsData:*		=	null;
		
		private var callbackFuncId : int		=	0;
		
		private var callbackFuncStore : Object	=	new Object();
		
		private var connectingInterval:uint		=	0;
		
		private var reconnectCounter:int		=	0;
					
		public function SNetwork( host : MIAppStarter )
		{
			Security.allowDomain("*");
			
			hostObject	=	host;
			
			connect();
		}
		
		public function counterReconnect():int
		{
			return reconnectCounter;
		}
		
		private function reconnect():void
		{
			// Clear last timeout event for case has an interrupt connect call from outside
			if( connectingInterval != 0 )
				clearTimeout( connectingInterval );
			
			// Make asynchorous call
			connectingInterval	=	setTimeout( doConnect, 1333 );
			
			function doConnect():void
			{
				reconnectCounter++;
				trace( "SNetwork.reconnect" );
				if( connect() )
					if( lastSentParamsData != null )
						send( lastSentParamsData );
			}
		}
		
		private function securityErrorHandler( event:SecurityErrorEvent ) : void 
		{
			var error:SError;
			if( event.errorID == 2048 )	// Flash Error #2048 : Security sandbox violation: %1 cannot load data from %2.
			{
				error	=	new SError( SError.ERR_NETWORK_FAILED, null, event.text );
				trace( "SNetwork.securityErrorHandler #2048" + error.getDetailMessage() );
				reconnect();
			}
			else
			{
				error	=	new SError( SError.ERR_NETWORK_CONNECT, null, serverInfo.uri );
				trace( "SNetwork.securityErrorHandler " + error.getDetailMessage() );
				reconnect();
			}
		}
		
		private function netStatusHandler(event:NetStatusEvent) : void 
		{
			var error:SError;
			switch( event.info.code )
			{
				case "NetConnection.Call.BadVersion":
					error	=	new SError( SError.ERR_NETWORK_CONNECT, null, "Server Syntax Error" );	
					trace( "NetConnection.Call.BadVersion " + error.getDetailMessage() );
					throw error;
					break;
				
				case "NetConnection.Call.Failed":
					error	=	new SError( SError.ERR_NETWORK_FAILED, null, event.info.details );
					trace( "NetConnection.Call.Failed " + error.getDetailMessage() );
					reconnect();					
					break;
				
				default:
					trace( "SNetwork.netStatusHandler " + event.info.code );
					break;
			}
		}
		
		private function IOErrorHandler( event:IOErrorEvent ) : void 
		{
			var error:SError	=	new SError( SError.ERR_NETWORK_FAILED, null, event.text );
			trace( "SNetwork.IOErrorHandler " + error.getDetailMessage() );
			reconnect();
		}
		
		private function asyncErrorEvent( event:AsyncErrorEvent ) : void 
		{
			trace( "SNetwork.asyncErrorEvent " );
			event.stopImmediatePropagation();
		}
		
		// Callback function for failed PRC call
		private function onFaultHandler( fault : Object ) : void
		{
			var error:SError	=	new SError( SError.ERR_AFM_RESPONSE_FAULT, null, fault.faultString, fault.faultDetail );
			trace( "SNetwork:onFaultHandler " + error.getDetailMessage() );
			reconnect();
		}
		
		// Callback function for succeeded PRC call 
		private function onResultHandler( result : Object ) : void
		{	
			// Server internal error. Response success but NULL data
			if( result == null )
			{
				// Dispatch error call
				if( globalResponseListener != null )					
					globalResponseListener.call( null, SCore.ERROR_SERVER_NULL, 0, -1, null, null );
					
				return;
			}
			
			// Allow module starting next transection
			lastSentParamsData	=	null;
			
			// Handle application logic error
			if( result.session.error < 0 )
			{
				// Dispatch error call
				if( globalResponseListener != null )
				{
					var mid : int		=	-1;
					var cid : int		=	-1;
					var params : Object	=	null;
					
					// Find which message error
					for each( var data : Object in result.params )
					{
						if( data.error < 0 )
						{
							mid		=	data.mid;
							cid		=	data.cid;
							params	=	data.params;
							break;
						}
					}
					
					globalResponseListener.call( null, result.session.error, mid, cid, result.session.msg, params );
				}
			}			
			else				
			{
				// Update server authenticate info when changed
				var sObj : Object	=	hostObject.getSessionParamsPtr();
				
				// Session information
				sObj['ras']			=	result.session.ras;
				sObj['servertime']	=	result.session.servertime;
				
				hostObject.setSessionParamsPtr( sObj );
				
				// Store back server response data
				for( var i:String in result.params )
				{
					var caller : Object	=	callbackFuncStore[result.params[i].cid];
					if( caller )
					{
						// Dispatch call to application
						if( caller.handle )
							caller.handle.call( null, result.params[i].mid, result.params[i].params );
						
						// Dispatch call to global listener 
						if( globalResponseListener != null )
							globalResponseListener.call( null, result.params[i].error, result.params[i].mid, result.params[i].cid, result.params[i].msg, result.params[i] );
						
						delete callbackFuncStore[result.params[i].cid];
					}
				}
			}
		}
		
		//////////////////////////////////////////////////
				
		// Call to send data to server by RPC call
		private function send( params : * ) : void
		{
			lastSentParamsData			=	params;
			
			var responder:Responder		=	new Responder( onResultHandler, onFaultHandler );
			service.call( serverInfo.servicePath, responder, params );
		}
		
		//////////////////////////////////////////////////		
		
		// Add message to queue. Message is hold on or send immediately to server  
		public function addBucket( sessionParams:Array, resHandler:Function, msgId:int, params:*, priority:int = 5.0 ) : int
		{			
			var buckObj : Object	=	new Object();
			
			buckObj.callbackId		=	callbackFuncId++;
			buckObj.msgId			=	msgId;
			buckObj.sessionParams	=	sessionParams;
			
			if( params == null )
				params	=	new Array();
			
			buckObj.params		=	params;
			
			// Update time to send
			if( priority < requestTimeDestination ) 
				requestTimeDestination	=	priority;
			
			reqBucketData.push( buckObj );
			
			// Save callback function to store
			var caller : Object			=	new Object();
			caller.handle				=	resHandler;			
			callbackFuncStore[buckObj.callbackId]	=	caller;
			
			updateNetStatus();
			
			return callbackFuncId;
		}
		
		public function overrideBucket( sessionParams:Array, resHandler:Function, msgId:int, params:*, priority:int = 5.0 ) : int
		{
			for( var i:String in reqBucketData )
			{
				if( reqBucketData[i].msgId == msgId )
				{					
					reqBucketData[i].params	=	params;
					
					// Update time to send
					if( priority < requestTimeDestination ) 
						requestTimeDestination	=	priority;
					
					if( callbackFuncStore.hasOwnProperty( reqBucketData[i].callbackId ) )
						callbackFuncStore[reqBucketData[i].callbackId].handle			=	resHandler;
					
					return reqBucketData[i].callbackId;
				}
			}
			
			return addBucket( sessionParams, resHandler, msgId, params, priority );			
		}
		
		public function callService( uri : String, serviceName : String, methodName : String, responseFunc : Function, params : Array ) : void
		{
			var error:SError;
			var customClient:Object = new Object(); 
			customClient.onMetaData = metaDataHandler;
			
			// Main object use to make RPC request			
			var ser : NetConnection		=	new NetConnection();
			ser.addEventListener( SecurityErrorEvent.SECURITY_ERROR, securityHandler);
			ser.addEventListener( NetStatusEvent.NET_STATUS, netStatus );
			//ser.client:Object			=	customClient;
			//ser.addEventListener( AsyncErrorEvent.ASYNC_ERROR, asyncErrorHandler );
			ser.objectEncoding	=	ObjectEncoding.AMF3;	
			
			try
			{
				ser.connect( uri );
				
				var responder:Responder		=	new Responder( onResult, onFault );
				var argms : Array			=	[serviceName + "." + methodName, responder, params];
	
				ser.call.apply( null, argms );
			}
			catch( ex : * )
			{
				error	=	new SError( SError.ERR_NETWORK_CONNECT, ex, uri );
				trace( "SNetwork.callService " + error.getDetailMessage() );
				if( responseFunc != null )
					responseFunc.call( null, -1, null );
			}
			
			function onResult( result : Object ) : void 
			{
				if( responseFunc != null )
					responseFunc.call( null, 0, result );
			}
			
			function onFault( fault : Object ) : void 
			{
				if( responseFunc != null )
					responseFunc.call( null, -1, fault );
			}
			
			function securityHandler( event:SecurityErrorEvent ) : void 
			{
				error	=	new SError( SError.ERR_NETWORK_CONNECT, null, serverInfo.uri );
				if( responseFunc != null )
					responseFunc.call( null, -1, null );
			}
			
			function netStatus(event:NetStatusEvent) : void 
			{
				switch( event.info.code )
				{
					case "NetConnection.Call.Failed":
						error	=	new SError( SError.ERR_NETWORK_CONNECT, null, event.info.details );
						if( responseFunc != null )
							responseFunc.call( null, -1, null );
						break;
				}
			}
			
			function metaDataHandler(infoObject:Object):void
			{
				trace( "Error Async " );
			}
		}		
		
		// Callback on ENTER_FRAME event
		public function updateFrame( deltaTime : Number, totalTime : Number ) : void
		{
			requestTimeMark	+=	deltaTime;
			
			if( lastSentParamsData == null && requestTimeMark >= requestTimeDestination )
			{			
				// Split and send message to server
				while( reqBucketData.length > 0 )						
					send( packData( reqBucketData ) );					
				
				//			
				updateNetStatus();
				
				// Start new circle
				requestTimeMark	=	0;
				
				// Reset to medium priority
				requestTimeDestination	=	ShareConstant.NET_PRIORITY_MEDIUM;
			}			
		}
				
		private function packData( data : Array ) : Array
		{
			if( data.length == 0 )
				return data;
			
			var remained:Array	=	new Array();		
			var buckets : Array	=	new Array();
			
			// Session information		
			buckets['session']		=	data[0].sessionParams;
			
			// Data information
			var content : Array		=	new Array();
			buckets['params']		=	content;	
			
			while( data.length > 0)
			{	
				var piece:Object		=	data.shift();
				
				// Test if this bucket session param equilance for this package				
				if( hostObject.equivalentSession( piece.sessionParams, buckets.session ) == false )
				{
					remained.push( piece );
					continue;
				}
				
				var bucket : Array		=	new Array();
				bucket['mid']			=	piece.msgId;
				bucket['cid']			=	piece.callbackId;
				bucket['params']		=	piece.params;
				
				content.push( bucket );
			}
			
			// Push remain bucket for next package
			while( remained.length > 0 )
				data.push( remained.shift() );
			
			return buckets;
		}
		
		public function flush() : void
		{
			requestTimeDestination	=	ShareConstant.NET_PRIORITY_IMMEDIATELY;
		}
		
		////////////////////////////////////////////////////	
		
		private function updateNetStatus() : void
		{
			if( netStatusCallbackFunc != null )
			{
				if( reqBucketData.length == 0 )
					netStatusCallbackFunc.call( null, true );
				
				else
					netStatusCallbackFunc.call( null, false );
			}
		}
		
		public function addNetStatusListener( func : Function ) : void
		{
			netStatusCallbackFunc		=	func;
		}
		
		public function addGlobalResponseListener( func : Function ) : void
		{
			globalResponseListener	=	func;
		}
		
		public function connect() : Boolean
		{
			try
			{
				// Close last connect if exist
				if( service != null )
				{
					// Note: Do not remove event listenner of last NetConnection
					// RemoveEventListener praise unhandle exception error
					// Ensure check event target to decide reconnect or not
					
					//service.removeEventListener( SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
					//service.removeEventListener( NetStatusEvent.NET_STATUS, netStatusHandler );
					//service.removeEventListener( IOErrorEvent.IO_ERROR, IOErrorHandler );
					
					service.close();
					service	=	null;
				}
				
				// Update new gateway information
				var network : Object	=	hostObject.getAMFGatewayInfo();
				
				serverInfo				=	new Object();
				serverInfo.uri			=	network["gateway"];
				serverInfo.servicePath	=	network["service_name"] + "." +  network["service_method"];			
			
				// Main object use to make RPC request			
				service					=	new NetConnection();
				service.addEventListener( SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler, false, 0, true );
				service.addEventListener( NetStatusEvent.NET_STATUS, netStatusHandler, false, 0, true );
				service.addEventListener( IOErrorEvent.IO_ERROR, IOErrorHandler, false, 0, true );
				service.addEventListener( AsyncErrorEvent.ASYNC_ERROR, asyncErrorEvent, false, 0, true );
				service.objectEncoding	=	ObjectEncoding.AMF3;

				// Make connection
				//service.connect( serverInfo.uri );
			}
			catch( ex : * )
			{
				var error:SError	=	new SError( SError.ERR_NETWORK_CONNECT, ex, serverInfo.uri );
				trace( "SNetwork.connect " + error.getDetailMessage() );
				
				reconnect();				
					
				return false;
			}
			
			return true;
		}
	}
}